import datetime
import os.path
import pickle
import time
from decimal import Decimal

import pandas as pd
import pytz
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.schedulers.background import BackgroundScheduler

from utils.data.loader import BinanceLoader
from typing import *
from loguru import logger
from config import ROOT_DIR, STRATEGY_LOG
from utils.strategy.conf.actualConf import ActualConf
import sqlite3
import sys

from utils.tools import NullStdout
from utils.trade.tradeType import ActualTrader
from globalVar import GlobalVar


class ActualExecutor:
    """

    """

    def __init__(self, conf: ActualConf):
        # 账户别名
        self.name = conf.account_name
        self.conf: ActualConf = conf
        self._api_key: str = conf.api_key
        self._secret_key: str = conf.secret_key
        self.aps: BlockingScheduler = BlockingScheduler(timezone=pytz.utc)

        self.loader: BinanceLoader = BinanceLoader(symbols=conf.strategy.symbols)
        self.traders: ActualTrader = ActualTrader(self._api_key, self._secret_key)

        self.stdout_bak = sys.stdout
        # stdout文件
        self.logfile = open(
            os.path.join(ROOT_DIR, 'locals', 'output', f'stdout-{self.name}.log'), 'a+',
            encoding='utf-8') if STRATEGY_LOG else NullStdout()

        self.exe_time: int = 0
        self._execute("""create table if not exists custom_data (
    id text primary key ,
    dt text,
    data blob
    )""")

    def _sql_connect(self) -> sqlite3.Connection:
        """
        获取数据库连接
        :return:
        """
        return sqlite3.connect(os.path.join(ROOT_DIR, f'locals/data/{self.name}.db'))

    def _execute(self, sql: str, params: Sequence[Any] = None) -> Tuple[Sequence[Any], List[str]]:
        """
        执行sql
        :param sql:
        :param params:
        :return:
        """
        if params is None:
            params = tuple()
        conn = self._sql_connect()
        try:
            cursor = conn.cursor()
            cursor.execute(sql, params)
            res: Sequence[Tuple[Any]] = cursor.fetchall()
            des: List[str] = [x[0] for x in cursor.description] if cursor.description is not None else []
            return res, des
        except sqlite3.Error as err:
            logger.error(err)
            conn.rollback()
        finally:
            conn.commit()
            conn.close()

    def load_custom_data(self) -> Dict[Any, Any]:
        """
        加载自定义数据
        :return:
        """
        data, col = self._execute("select data from custom_data where id ='%s'" % self.name)
        if len(data) == 0:
            return dict()
        else:
            return pickle.loads(data[0][0])

    def dump_custom_data(self, data: bytes) -> None:
        """
        存储自定义数据
        :param data:
        :return:
        """
        self._execute("insert or replace into custom_data (id, dt, data) values (?, ?, ?)",
                      [self.name, datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'), data])

    @staticmethod
    def update_price(data: pd.DataFrame):
        """
        更新公共数据中的价格数据
        :param data:
        :return:
        """
        # 更新币种最新价格为当前K线数据
        spot = data[data['trade_type'] == 0]
        GlobalVar.SPOT_PRICE |= {x: Decimal(y) for x, y in zip(spot['symbol'], spot['close'])}
        swap = data[data['trade_type'] == 1]
        GlobalVar.SWAP_PRICE |= {x: Decimal(y) for x, y in zip(swap['symbol'], swap['close'])}

    def step(self):
        """
        :return:
        """
        sys.stdout = self.logfile
        # 获取数据
        df = self.conf.load_data(self.exe_time)
        logger.info(f'数据获取完成，symbols：{self.conf.strategy.symbols}，count：{self.conf.strategy.historyKlineCount}')
        # 更新公共数据
        self.update_price(df[df['open_time'] == (self.exe_time - self.conf.period) * 1000])

        if self.conf.strategy.is_execute():
            self.conf.strategy.injection({'factors': self.conf.strategy.factorFunc})
            df = self.conf.strategy.pick(df)
            logger.info(f'信号计算完成')
            # 注入自定义和接口
            self.conf.strategy.injection({
                'custom': self.load_custom_data(),
                'spot_trade': self.traders.spot_trade,
                'swap_trade': self.traders.swap_trade,
                'transfer': self.traders.transfer,
                'margin_type': self.traders.margin_type,
                'position_side': self.traders.position_side,
                'swap_leverage': self.traders.swap_leverage,
                'spot_balance_interface': self.traders.exchange.spot_balance,
                'swap_balance_interface': self.traders.exchange.swap_balance,
                'spot_price': lambda: self.traders.exchange.spot_symbol_prices.copy(),
                'swap_price': lambda: self.traders.exchange.swap_symbol_prices.copy(),
                'spot_balance': self.traders.exchange.spot_balance,
                'swap_balance': self.traders.exchange.swap_balance,
                'swap_position': self.traders.exchange.swap_position,
                'is_backtest': False
            })
            self.conf.strategy.trade(df)
            logger.info(f'交易执行完成')
            # 保存策略的自定义数据
            self.dump_custom_data(self.conf.strategy.custom())
        sys.stdout = self.stdout_bak
        self.logfile.flush()

    @staticmethod
    def _trade_info(exe: "ActualExecutor"):
        """
        更新交易规范信息
        :param exe:
        :return:
        """
        GlobalVar.SPOT_INFO = exe.traders.exchange.spot_exchange_info
        GlobalVar.SWAP_INFO = exe.traders.exchange.swap_exchange_info
        logger.info(f'更新交易规范信息完成')

    @staticmethod
    def _job(exe: "ActualExecutor"):
        """
        定时任务
        :param exe:
        :return:
        """
        exe.exe_time = int(time.time())
        GlobalVar.EXE_TIMESTAMP = exe.exe_time
        logger.info(f'触发任务执行，执行时间：{exe.exe_time}')
        if exe.exe_time < exe.conf.s_time.timestamp() or exe.exe_time > exe.conf.e_time.timestamp():
            logger.warning(f'配置执行时间为：{exe.conf.s_time}至{exe.conf.e_time}，已跳过')
            return
        # 注入运行时间到策略中
        exe.conf.strategy.injection({'execute_timestamp': exe.exe_time})
        if not exe.conf.strategy.is_execute():
            logger.info(f'跳过执行')
            return
        try:
            exe.step()
        except Exception as e:
            raise e
            # logger.error(f'执行失败，理论执行时间：{exe.exe_time}，'
            #              f'{datetime.datetime.fromtimestamp(exe.exe_time, tz=datetime.timezone.utc)}，'
            #              f'失败信息：{e.__str__()}')

    def run(self) -> None:
        # 读取key和密钥
        logger.info(f'使用账号apiKey：{self._api_key[:5] + "..."}，SecretKey：{self._secret_key[:5] + "..."}')
        # 检查key权限
        flag, msg = self.traders.check_permission()
        if not flag:
            logger.critical(f'权限检查未通过，账号：{self._api_key[:5]}..., {msg}')
            sys.exit(1)
        logger.info(f'权限检测完成')

        symbols = self.conf.strategy.symbols
        if len(symbols) == 0:
            symbols = list(self.traders.exchange.swap_symbol_prices.keys())
        resp = self.traders.swap_leverage({x: self.conf.strategy.leverage for x in symbols})
        for k, v in resp.items():
            if not v:
                logger.error(f'调整杠杆失败：{k}')
                sys.exit()
        logger.info(f'杠杆调整完成：{"*" if len(symbols) > 1 else symbols}，杠杆：{self.conf.strategy.leverage}')
        self.traders.margin_type(symbols, 'ISOLATED')
        logger.info('修改保证金模式完成：ISOLATED')
        self.traders.position_side(self.conf.strategy.positionSide)
        logger.info(f'仓位模式调整完成：{self.conf.strategy.positionSide}')

        job_option = {self.conf.strategy.Kline: f'*/{self.conf.strategy.KlineBatchSize}'}
        logger.info(f'执行策略：\'{self.conf.strategy_name}\'')
        logger.info(f'全配置参数：{self.conf.strategy.system_conf}')
        logger.info(f'定时器参数：{job_option}')

        self._trade_info(self)
        self.aps.add_job(self._job, id='main', args=[self], kwargs={}, trigger='cron', replace_existing=True,
                         **job_option, timezone=pytz.utc)
        self.aps.add_job(self._trade_info, id='info', args=[self], trigger='cron', replace_existing=True, hour='*',
                         minute='59', second='50', timezone=pytz.utc)
        self.aps.start()
